	
//  Copyright (C) 2009 Luca Piccioni
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
// 
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

using Derm;
using Derm.Raster;
using Derm.Render;

namespace Derm
{
	/// <summary>
	/// ImageCodec for PPM file format.
	/// </summary>
	class PPMImageLoaderPlugin : ImageCodecPlugin
	{
		#region PPM File Format Utilities

		private enum PpmLayout
		{
			AsciiBitmap,
			AsciiGraymap,
			AsciiColorMap,
			BinaryBitmap,
			BinaryColorMap,
			BinaryGrayMap,
			FloatGrayMap,
			FloatColorMap,
			UknownColorMap,
		}

		/// <summary>
		/// Common PPM/PGM/PFM header.
		/// </summary>
		private struct PpmHeader
		{
			/// <summary>
			/// The PPM format identifier.
			/// </summary>
			public string Id;

			/// <summary>
			/// PPM format layout.
			/// </summary>
			public PpmLayout Layout
			{
				get {
					if      (Id == "P1")
						return (PpmLayout.AsciiBitmap);
					else if (Id == "P2")
						return (PpmLayout.AsciiGraymap);
					else if (Id == "P3")
						return (PpmLayout.AsciiColorMap);
					else if (Id == "P4")
						return (PpmLayout.BinaryBitmap);
					else if (Id == "P5")
						return (PpmLayout.BinaryGrayMap);
					else if (Id == "P6")
						return (PpmLayout.BinaryColorMap);
					else if (Id == "Pf")
						return (PpmLayout.FloatGrayMap);
					else if (Id == "PF")
						return (PpmLayout.FloatColorMap);
					else
						return (PpmLayout.UknownColorMap);
				}
			}

			/// <summary>
			/// PPM image format
			/// </summary>
			public ImageFormat ImageFormat
			{
				get {
					switch (Layout) {
						case PpmLayout.AsciiBitmap:
						case PpmLayout.BinaryBitmap:
							return (ImageFormat.Pbm);
						case PpmLayout.AsciiGraymap:
						case PpmLayout.BinaryGrayMap:
							return (ImageFormat.Pgm);
						case PpmLayout.AsciiColorMap:
						case PpmLayout.BinaryColorMap:
							return ImageFormat.Ppm;
						case PpmLayout.FloatGrayMap:
						case PpmLayout.FloatColorMap:
							return ImageFormat.Pfm;
						default:
							return ImageFormat.Unknown;
					}
				}
			}

			/// <summary>
			/// PPM pixel format.
			/// </summary>
			public Pixel.Type PixelFormat
			{
				get {
					switch (Layout) {
						case PpmLayout.AsciiBitmap:
						case PpmLayout.BinaryBitmap:

							return (Pixel.Type.GRAY8);

						case PpmLayout.AsciiGraymap:
						case PpmLayout.BinaryGrayMap:

							if (Math.Abs(Scale - 0.0) < Double.Epsilon)
								return (Pixel.Type.None);
							if      (Scale <= 0xFF)
								return (Pixel.Type.GRAY8);
							else if (Scale <= 0xFFFF)
								return (Pixel.Type.GRAY16);
							else
								return (Pixel.Type.None);

						case PpmLayout.AsciiColorMap:
						case PpmLayout.BinaryColorMap:
							
							if (Math.Abs(Scale - 0.0) < Double.Epsilon)
								return (Pixel.Type.None);
							if      (Scale <= 0xFF)
								return (Pixel.Type.RGB24);
							else if (Scale <= 0xFFFF)
								return (Pixel.Type.RGB48);
							else
								return (Pixel.Type.None);

						case PpmLayout.FloatGrayMap:
						case PpmLayout.FloatColorMap:
							
							return Pixel.Type.RGBF;

						default:
							return Pixel.Type.None;
					}
				}
			}

			/// <summary>
			/// Determine whether the layout uses binary encoding.
			/// </summary>
			public bool IsBinaryLayout
			{
				get {
					switch (Layout) {
						case PpmLayout.BinaryBitmap:
						case PpmLayout.BinaryGrayMap:
						case PpmLayout.BinaryColorMap:
						case PpmLayout.FloatGrayMap:
						case PpmLayout.FloatColorMap:
							return (true);
						default:
							return (false);
					}
				}
			}

			/// <summary>
			/// The map width, in pixels.
			/// </summary>
			public uint Width;

			/// <summary>
			/// The map height, in pixels.
			/// </summary>
			public uint Height;

			/// <summary>
			/// The pixel component scale (maximum value or value scale factor).
			/// </summary>
			public double Scale;

			/// <summary>
			/// Comments.
			/// </summary>
			public List<string> Comments;
		}

		private static string[] ExtractLineTokens(StreamReader sr)
		{
			if (sr == null)
				throw new ArgumentNullException("sr");

			byte[] whiteBuffer = new byte[1];
			long position = sr.BaseStream.Position;
			string pLine = sr.ReadLine();

			// Set real position after the end of the line (assumes only one new line character '0x0A')
			sr.BaseStream.Seek(position + pLine.Length, SeekOrigin.Begin);
			while (sr.EndOfStream == false) {
				sr.BaseStream.Read(whiteBuffer, 0, 1);
				if (whiteBuffer[0] == '\n')
					break;
			}

			// Trim line
			pLine = pLine.Trim();
			// Skip comment lines
			return (pLine.Split(new char[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries));
		}

		private static PpmHeader ExtractHeader(StreamReader sr)
		{
			PpmHeader ppmHeader;
			uint tokenLimit = 4;

			if (sr == null)
				throw new ArgumentNullException("sr");

			ppmHeader.Id = "";
			ppmHeader.Width = ppmHeader.Height = 0;
			ppmHeader.Scale = 0.0;
			ppmHeader.Comments = new List<string>();

			for (uint tokenCount = 0; tokenCount < tokenLimit;) {
				string[] tokens = ExtractLineTokens(sr);

				if ((tokens.Length > 0) && (tokens[0] != "#")) {
					foreach (string token in tokens) {
						switch (tokenCount) {
							case 0:
								ppmHeader.Id = token;
								tokenCount++;

								// Exception for P1/P4 formats (no scale specified)
								if ((ppmHeader.Layout == PpmLayout.AsciiBitmap) || (ppmHeader.Layout == PpmLayout.BinaryBitmap))
									tokenLimit = 3;
								break;
							case 1:
								if (UInt32.TryParse(token, System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out ppmHeader.Width) == false)
									throw new InvalidOperationException("raster width (" + token + ") is not a recognized integer value");
								tokenCount++;
								break;
							case 2:
								if (UInt32.TryParse(token, System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out ppmHeader.Height) == false)
									throw new InvalidOperationException("raster height (" + token + ") is not a recognized integer value");
								tokenCount++;
								break;
							case 3:
								if (Double.TryParse(token, System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out ppmHeader.Scale) == false)
									throw new InvalidOperationException("raster scale (" + token + ") is not a recognized integer/floating-point value");
								tokenCount++;
								break;
							default:
								// Do not parse more
								break;
						}
					}
				} else if (tokens.Length > 1) {
					StringBuilder sb = new StringBuilder();

					for (uint i = 1; i < tokens.Length; i++) {
						sb.Append(tokens[1]);
						sb.Append(" ");
					}
					sb.Remove(sb.Length - 1, 1);
					sb.AppendLine();

					ppmHeader.Comments.Add(sb.ToString());
				}
			}

			return (ppmHeader);
		}

		#endregion

		#region ImageCodecPlugin Implementation

		/// <summary>
		/// Plugin name.
		/// </summary>
		public override string Name { get { return ("Derm.PPM"); } }

		/// <summary>
		/// Determine whether this plugin is available for the current process.
		/// </summary>
		/// <returns>
		/// It returns a boolean value indicating whether the plugin is available for the current
		/// process.
		/// </returns>
		public override bool CheckAvailability()
		{
			return (true);
		}

		/// <summary>
		/// Get a list of image formats supported for reading.
		/// </summary>
		public override IEnumerable<ImageFormat> SupportedReadImageFormats
		{
			get {
				return (sSupportedReadFormats);
			}
		}

		/// <summary>
		/// Get a list of image formats supported for writing.
		/// </summary>
		public override IEnumerable<ImageFormat> SupportedWriteImageFormats
		{
			get {
				return (null);
			}
		}

		/// <summary>
		/// Currently supported image formats for reading.
		/// </summary>
		private static ImageFormat[] sSupportedReadFormats = new ImageFormat[] {
			ImageFormat.Pbm,
			ImageFormat.Pgm,
			ImageFormat.Ppm,
			ImageFormat.Pfm
		};

		/// <summary>
		/// Query image file informations.
		/// </summary>
		/// <returns>
		/// A <see cref="ImageCodec.ImageInfo"/>
		/// </returns>
		public override ImageInfo QueryImageInfo(Stream fs, ImageCodec.CodecCriteria criteria)
		{
			using (StreamReader sr = new StreamReader(fs)) {
				ImageInfo mInfo = new ImageInfo();
				PpmHeader ppmHeader = ExtractHeader(sr);

				mInfo.ContainerFormat = ppmHeader.ImageFormat;
				mInfo.Width = ppmHeader.Width;
				mInfo.Height = ppmHeader.Height;
				mInfo.PixelType = ppmHeader.PixelFormat;

				return (mInfo);
			}
		}

		/// <summary>
		/// Load image pixels from file.
		/// </summary>
		/// <returns>
		/// A <see cref="Image"/>
		/// </returns>
		/// <param name="criteria">
		/// A <see cref="ImageCodec.CodecCriteria"/> specifying parameters for loading an image stream.
		/// </param>
		public override Image Load(Stream fs, ImageCodec.CodecCriteria criteria)
		{
			PpmHeader ppmHeader;

			using (StreamReader sr = new StreamReader(fs)) {
				ppmHeader = ExtractHeader(sr);
			}

			if (ppmHeader.Layout == PpmLayout.UknownColorMap)
				throw new InvalidOperationException("unknown map layout");
			if (ppmHeader.PixelFormat == Pixel.Type.None)
				throw new InvalidOperationException("unknown map format");

			Image image = Image.CreateImage(ppmHeader.Width, ppmHeader.Height, ppmHeader.PixelFormat);

			if ((ppmHeader.Layout != PpmLayout.AsciiBitmap) && (ppmHeader.Layout != PpmLayout.BinaryBitmap)) {

				// Common implementation for ascii/binary PGM/PPM/PFM formats

				if (ppmHeader.IsBinaryLayout == true) {
					GCHandle hImageData = GCHandle.Alloc(image.GetPixelData(), GCHandleType.Pinned);

					try {
						byte[] scanline = new byte[image.Stride];

						unsafe {
							byte* hImageDataPtr = (byte*)hImageData.AddrOfPinnedObject().ToPointer(), hImageDataPtrCursor = hImageDataPtr + image.Stride * (image.Height - 1);

							for (uint i = 0; i < image.Height; i++, hImageDataPtrCursor -= image.Stride) {
								int red = fs.Read(scanline, 0, (int)image.Stride);

								if (red != image.Stride)
									throw new EndOfStreamException("PPM map data truncated");

								Marshal.Copy(scanline, 0, new IntPtr((void*)hImageDataPtrCursor), scanline.Length);
							}

							if ((ppmHeader.Layout == PpmLayout.FloatGrayMap) || (ppmHeader.Layout == PpmLayout.FloatColorMap)) {
								float scale = (float)Math.Abs(ppmHeader.Scale);
							
								// PFM format requires additional processing

								// Swap bytes, if necessary

								// Scale values
								float *hImageFloatDataPtr = (float*)hImageDataPtr;

								for (uint i = 0; i < image.Width * image.Height * Pixel.GetComponentsCount(image.PixelFormat); i++)
									hImageFloatDataPtr[i] *= scale;
							}
						}
					} finally {
						hImageData.Free();
					}
				} else {
					uint components = Pixel.GetComponentsCount(image.PixelFormat);

					if (components > 3)
						throw new InvalidOperationException("unexpected image color format " + image.PixelFormat);

					using (StreamReader sr = new StreamReader(fs)) {
						GCHandle hImageData = GCHandle.Alloc(image.GetPixelData(), GCHandleType.Pinned);

						try {
							unsafe {
								byte* hImageDataPtr = (byte*)hImageData.AddrOfPinnedObject().ToPointer();
								byte* hImageDataPtrCursor = hImageDataPtr + image.Stride * (image.Height - 1), hImageDataPtrEnd = hImageDataPtr;
								uint wCursor = 0, hCursor = 1;

								while ((sr.EndOfStream == false) && (hImageDataPtrCursor >= hImageDataPtrEnd)) {
									string scanline = sr.ReadLine();
									string[] tokens = scanline.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

									for (uint i = 0; (i < tokens.Length) && (hImageDataPtrCursor >= hImageDataPtrEnd); i++) {
										uint value;

										if (UInt32.TryParse(tokens[i], System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out value) == false)
											throw new InvalidOperationException("raster data value (" + tokens[i] + ") is not a recognized integer value");
										if (value < 0)
											throw new InvalidOperationException("raster data value (" + tokens[i] + ") has a negative value");
										if (value > ppmHeader.Scale)
											throw new InvalidOperationException("raster data value (" + tokens[i] + ") has a value greater than the scale (" + ppmHeader.Scale.ToString("d") + ")");

										switch (image.PixelFormat) {
											case Pixel.Type.GRAY8:
											case Pixel.Type.RGB24:
												*((byte*)hImageDataPtrCursor) = (byte)((value / ppmHeader.Scale) * byte.MaxValue);
												hImageDataPtrCursor += 1;
												break;
											case Pixel.Type.GRAY16:
											case Pixel.Type.RGB48:
												*((ushort*)hImageDataPtrCursor) = (ushort)((value / ppmHeader.Scale) * ushort.MaxValue);
												hImageDataPtrCursor += 2;
												break;
											default:
												throw new InvalidOperationException("invalid pixel format " + image.PixelFormat);
										}

										if ((++wCursor % (image.Width * components)) == 0) {
											hCursor++;
											hImageDataPtrCursor = hImageDataPtr + image.Stride * (image.Height - hCursor);
										}
									}
								}
							}
						} finally {
							hImageData.Free();
						}
					}
				}
			} else {
				
				// PBM format requires different implementation

				if (ppmHeader.IsBinaryLayout == true) {

					GCHandle hImageData = GCHandle.Alloc(image.GetPixelData(), GCHandleType.Pinned);

					try {
						byte[] scanline = new byte[(image.Width + 7) / 8];

						unsafe {
							byte* hImageDataPtr = (byte*)hImageData.AddrOfPinnedObject().ToPointer();

							for (uint i = 0; i < image.Height; i++) {
								int red = fs.Read(scanline, 0, (int)scanline.Length);
								uint wbits = 0;

								byte* hImageDataPtrCursor = hImageDataPtr + image.Stride * (image.Height - i - 1);

								foreach (byte b in scanline) {
									for (int idx = 7; (idx >= 0) && (wbits < image.Width); idx--, wbits++, hImageDataPtrCursor++)
										*hImageDataPtrCursor = ((b & (1 << idx)) == 0) ? (byte)255 : (byte)0;
								}
							}
						}
					} finally {
						hImageData.Free();
					}

				} else {
					
					ColorGRAYF color = new ColorGRAYF();
					uint x = 0, y = image.Height;

					using (StreamReader sr = new StreamReader(fs)) {
						while ((sr.EndOfStream == false)) {
							string scanline = sr.ReadLine();
							string[] tokens = scanline.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

							for (uint i = 0; i < tokens.Length; i++) {
								uint value;

								if (UInt32.TryParse(tokens[i], System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out value) == false)
									throw new InvalidOperationException("raster data value (" + tokens[i] + ") is not a recognized integer value");
								if (value != 0 && value != 1)
									throw new InvalidOperationException("raster data value (" + tokens[i] + ") has a value different than 0 or 1");
									
								color[0] = (float)(1 - value);

								// Advance by line
								if (x == 0) y--;
								// Assign pixel
								image[x, y] = color;
								// Check end of image
								if ((y == 0) && (x +1 == image.Width))
									break;
								// Advance by pixel
								x = ++x % image.Width;
							}
						}
					}

				}
			}

			return (image);
		}

		/// <summary>
		/// Save image pixels to file.
		/// </summary>
		/// <param name="fs">
		/// A <see cref="System.IO.FileStream"/> which stores the image data.
		/// </param>
		/// <param name="image">
		/// A <see cref="Image"/> holding the data to be stored.
		/// </param>
		/// <param name="criteria">
		/// A <see cref="ImageCodec.CodecCriteria"/> specifying parameters for loading an image stream.
		/// </param>
		public override void Save(Stream fs, Image image, ImageFormat format, ImageCodec.CodecCriteria criteria)
		{
			throw new NotImplementedException();
		}

[System.Security.SuppressUnmanagedCodeSecurity()]
		[System.Runtime.InteropServices.DllImport(Library, EntryPoint = "memcpy", ExactSpelling = true)]
		internal extern static unsafe void memcpy(void *dst, void *src, int bytes);

#if WIN32
		/// <summary>
		/// Library exporting C memcpy.
		/// </summary>
		private const string Library = "msvcrt.dll";
#endif

#if __linux__
		/// <summary>
		/// Library exporting C memcpy.
		/// </summary>
		private const string Library = "libstdc.so";
#endif

		#endregion
	}
}
